package ferp.center.server.entity;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Text;
import com.google.gson2.reflect.TypeToken;
import ferp.center.server.Action;
import ferp.core.game.Game;

import javax.persistence.*;
import java.lang.reflect.Type;
import java.util.*;

/**
 * User: igorgok
 * Date: 5/28/13 9:49 AM
 */

@NamedQueries({
    @NamedQuery(name = Range.QUERY_GET_ALL,
        query = "SELECT r FROM Range r")
})

@Entity
public class Range
{
  // get all ranges
  public static final String QUERY_GET_ALL = "getAll";

  public static final class Child
  {
    public long key;
    public int size;
    public long max;

    public Child(long key, int size, long max)
    {
      this.key = key;
      this.size = size;
      this.max = max;
    }

    protected Child() {}
  }

  public static class Children
  {
    public void add(long key, int size, long score)
    {
      Child child = new Child(key, size, score);

      sorted.put(child.max, child);
      map.put(child.key, child);
    }

    public void update(Range r)
    {
      Child child = map.get(r.getKey().getId());

      // in some cases the range is not there (why?), so add it
      if (child == null)
      {
        add(r.getKey().getId(), r.getSize(), r.getMax());

        Action.logger().info("update: had to add range " + r.getKey() + " of size " + r.getSize() + ", max=" + r.getMax());
      }
      else
        child.size = r.getSize();
    }

    public void remove(long key)
    {
      Child child = map.remove(key);

      if (child != null)
        sorted.remove(child.max);
    }

    public int size() {return sorted.size();}

    public Collection<Child> sorted()
    {
      LinkedList<Child> list = new LinkedList<Child>();

      for (Map.Entry<Long, Child> e : sorted.entrySet())
        list.add(e.getValue());

      return list;
    }

    public Child first() {return sorted.get(sorted.firstKey());}

    public Children split()
    {
      Range.Children c2 = new Range.Children();

      // split the list and update both ranges sizes
      for (int i = 0, half = size() / 2; i < half; ++i)
      {
        Range.Child child = sorted.remove(sorted.lastKey());

        c2.add(child.key, child.size, child.max);
      }

      return c2;
    }

    private Children()
    {
      this.sorted = new ReversedMap();
    }

    private Children(ReversedMap sorted)
    {
      this.sorted = sorted;

      // prepare the hash table
      for (Child child : sorted.values())
        map.put(child.key, child);
    }

    private final ReversedMap sorted;
    private final HashMap<Long, Child> map = new HashMap<Long, Range.Child>();
  }

  // get leaf ranges only
  @SuppressWarnings("unchecked")
  public static List<Range> leaves(EntityManager em, int offset, int chunk) {return em.createQuery("SELECT r FROM Range r WHERE r.level=0").setFirstResult(offset).setMaxResults(chunk).getResultList();}

  // create tree root range
  public static Range root() {return new Range(null, 1, 0, Long.MAX_VALUE, Long.MIN_VALUE);}

  // create new root range
  public static Range root(Range r1, Range r2)
  {
    Range r = new Range(null, r1.level + 1, r1.size + r2.size, r1.max, r2.min);
    ReversedMap children = new ReversedMap();

    children.put(r1.max, new Range.Child(r1.key.getId(), r1.size, r1.max));
    children.put(r2.max, new Range.Child(r2.key.getId(), r2.size, r2.max));

    r.update(children);

    return r;
  }

  // create leaf range
  public static Range leaf(Rating rating) {return new Range(null, 0, 0, rating.getValue(), rating.getValue());}
  // create new sibling
  public static Range sibling(Range sibling)
  {
    Range r = new Range();

    r.setParent(sibling.getParent());
    r.setLevel(sibling.getLevel());

    return r;
  }

  public Key getKey() {return key;}
  public void setParent(Key parent) {this.parent = parent;}
  public void setParent(Range parent) {this.parent = parent.getKey();}
  public Range getParent(EntityManager em) {return parent == null ? null : em.find(Range.class, parent);}
  public Key getParent() {return parent;}
  public void setSize(int size) {this.size = size;}
  public int getSize() {return size;}
  public void setLevel(int level) {this.level = level;}
  public int getLevel() {return level;}
  public void setMax(long max) {this.max = max;}
  public long getMax() {return max;}
  public void setMin(long min) {this.min = min;}
  public long getMin() {return min;}

  // get parsed children
  public Children children() {return new Children((ReversedMap)Game.gson.fromJson(getChildren(), ListOfChildren));}
  public void update(Children children) {update(children.sorted);}

  protected Range() {}

  protected Range(Key parent)
  {
    this.parent = parent;
  }

  protected Range(Key parent, int level, int size, long max, long min)
  {
    this(parent);

    this.level = level;
    this.size = size;
    this.max = max;
    this.min = min;
  }

  protected void setChildren(String children) {this.children = new Text(children);}
  protected String getChildren() {return children.getValue();}

  private static final class ReversedMap extends TreeMap<Long, Child>
  {
    public ReversedMap()
    {
      super(Collections.reverseOrder());
    }
  }

  private static final Type ListOfChildren = new TypeToken<ReversedMap>(){}.getType();
  private static final String EMPTY = Game.gson.toJson(new ReversedMap(), ListOfChildren);

  @Id
  @GeneratedValue(strategy= GenerationType.IDENTITY)
  private Key key;
  // parent range key
  private Key parent;
  // number of ratings in the subtree
  private int size;
  // tree level
  private int level;
  // max range value
  private long max;
  // min range value
  private long min;
  // JSON encoded children
  private Text children = new Text(EMPTY);

  private void update(ReversedMap children)
  {
    this.size = 0;

    // update size
    for (Child c : children.values())
      this.size += c.size;

    // convert back to JSON
    setChildren(Game.gson.toJson(children, ListOfChildren));
  }
}
